msg_tool\scripts\hexen_haus\archive/
odio.rs

1//! HexenHaus ODIO archive (.bin)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Result, anyhow};
6use std::io::{Read, Seek, SeekFrom};
7use std::sync::{Arc, Mutex};
8
9const ODIO_SIGNATURE: &[u8; 4] = b"ODIO";
10const HEADER_CHECK_OFFSET: u64 = 0x0A;
11const HEADER_CHECK_VALUE: u32 = 0xCCAE_01FF;
12const INDEX_START: u64 = 0x12;
13const INDEX_ENTRY_SIZE: u64 = 6;
14const ENTRY_HEADER_SIZE: u64 = 0x2C;
15
16#[derive(Debug)]
17/// HexenHaus ODIO archive builder
18pub struct HexenHausOdioArchiveBuilder;
19
20impl HexenHausOdioArchiveBuilder {
21    /// Creates a new `HexenHausOdioArchiveBuilder`
22    pub const fn new() -> Self {
23        HexenHausOdioArchiveBuilder
24    }
25}
26
27impl ScriptBuilder for HexenHausOdioArchiveBuilder {
28    fn default_encoding(&self) -> Encoding {
29        Encoding::Cp932
30    }
31
32    fn default_archive_encoding(&self) -> Option<Encoding> {
33        Some(Encoding::Cp932)
34    }
35
36    fn build_script(
37        &self,
38        buf: Vec<u8>,
39        _filename: &str,
40        _encoding: Encoding,
41        archive_encoding: Encoding,
42        config: &ExtraConfig,
43        _archive: Option<&Box<dyn Script>>,
44    ) -> Result<Box<dyn Script + Send + Sync>> {
45        Ok(Box::new(HexenHausOdioArchive::new(
46            MemReader::new(buf),
47            archive_encoding,
48            config,
49        )?))
50    }
51
52    fn build_script_from_file(
53        &self,
54        filename: &str,
55        _encoding: Encoding,
56        archive_encoding: Encoding,
57        config: &ExtraConfig,
58        _archive: Option<&Box<dyn Script>>,
59    ) -> Result<Box<dyn Script + Send + Sync>> {
60        if filename == "-" {
61            let data = crate::utils::files::read_file(filename)?;
62            return Ok(Box::new(HexenHausOdioArchive::new(
63                MemReader::new(data),
64                archive_encoding,
65                config,
66            )?));
67        }
68        let file = std::fs::File::open(filename)?;
69        let reader = std::io::BufReader::new(file);
70        Ok(Box::new(HexenHausOdioArchive::new(
71            reader,
72            archive_encoding,
73            config,
74        )?))
75    }
76
77    fn build_script_from_reader<'a>(
78        &self,
79        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
80        _filename: &str,
81        _encoding: Encoding,
82        archive_encoding: Encoding,
83        config: &ExtraConfig,
84        _archive: Option<&Box<dyn Script>>,
85    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
86        Ok(Box::new(HexenHausOdioArchive::new(
87            reader,
88            archive_encoding,
89            config,
90        )?))
91    }
92
93    fn extensions(&self) -> &'static [&'static str] {
94        &["bin"]
95    }
96
97    fn script_type(&self) -> &'static ScriptType {
98        &ScriptType::HexenHausOdio
99    }
100
101    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
102        if buf_len >= ODIO_SIGNATURE.len() && buf.starts_with(ODIO_SIGNATURE) {
103            Some(10)
104        } else {
105            None
106        }
107    }
108
109    fn is_archive(&self) -> bool {
110        true
111    }
112}
113
114#[derive(Debug, Clone)]
115struct HexenHausOdioEntry {
116    name: String,
117    offset: u64,
118    size: u64,
119}
120
121#[derive(Debug)]
122/// HexenHaus ODIO archive reader
123pub struct HexenHausOdioArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
124    reader: Arc<Mutex<T>>,
125    entries: Vec<HexenHausOdioEntry>,
126    _mark: std::marker::PhantomData<&'b ()>,
127}
128
129impl<'b, T: Read + Seek + std::fmt::Debug + 'b> HexenHausOdioArchive<'b, T> {
130    /// Creates a new `HexenHausOdioArchive`
131    pub fn new(mut reader: T, _archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
132        reader.seek(SeekFrom::Start(0))?;
133        let mut signature = [0u8; 4];
134        reader.read_exact(&mut signature)?;
135        if signature != *ODIO_SIGNATURE {
136            return Err(anyhow!("Invalid HexenHaus ODIO signature"));
137        }
138
139        let reserved = reader.read_u32()?;
140        if reserved != 0 {
141            return Err(anyhow!("Unexpected reserved field in ODIO header"));
142        }
143
144        reader.seek(SeekFrom::Start(HEADER_CHECK_OFFSET))?;
145        let header_check = reader.read_u32()?;
146        if header_check != HEADER_CHECK_VALUE {
147            return Err(anyhow!("Invalid HexenHaus ODIO header check value"));
148        }
149
150        let file_length = reader.seek(SeekFrom::End(0))?;
151        reader.seek(SeekFrom::Start(INDEX_START))?;
152        let first_offset = u64::from(reader.read_u32()?);
153        if first_offset < INDEX_START {
154            return Err(anyhow!("First entry offset precedes index start"));
155        }
156        if first_offset > file_length {
157            return Err(anyhow!("First entry offset exceeds file length"));
158        }
159
160        let index_len = first_offset
161            .checked_sub(INDEX_START)
162            .ok_or_else(|| anyhow!("Invalid index length in ODIO archive"))?;
163        if index_len % INDEX_ENTRY_SIZE != 0 {
164            return Err(anyhow!("ODIO index length is not aligned"));
165        }
166        let entry_count_u64 = index_len / INDEX_ENTRY_SIZE;
167        let entry_count =
168            usize::try_from(entry_count_u64).map_err(|_| anyhow!("ODIO entry count overflow"))?;
169        if entry_count == 0 {
170            return Err(anyhow!("ODIO archive contains no entries"));
171        }
172
173        let mut entries = Vec::with_capacity(entry_count);
174        let mut index_offset = INDEX_START;
175        let mut next_offset = first_offset;
176
177        for i in 0..entry_count {
178            let entry_offset = next_offset;
179
180            index_offset = index_offset
181                .checked_add(INDEX_ENTRY_SIZE)
182                .ok_or_else(|| anyhow!("Index offset overflow"))?;
183
184            if i + 1 == entry_count {
185                next_offset = file_length;
186            } else {
187                if index_offset + 4 > file_length {
188                    return Err(anyhow!("Index offset exceeds file length"));
189                }
190                reader.seek(SeekFrom::Start(index_offset))?;
191                next_offset = u64::from(reader.read_u32()?);
192            }
193
194            if entry_offset > file_length {
195                return Err(anyhow!("Entry offset exceeds file length"));
196            }
197            if next_offset > file_length {
198                return Err(anyhow!("Entry extends beyond file length"));
199            }
200            if next_offset < entry_offset {
201                return Err(anyhow!("Entry offsets are out of order"));
202            }
203
204            let size = next_offset - entry_offset;
205            if size == 0 {
206                continue;
207            }
208
209            let name = format!("{:04}.ogg", i);
210            entries.push(HexenHausOdioEntry {
211                name,
212                offset: entry_offset,
213                size,
214            });
215        }
216
217        if entries.is_empty() {
218            return Err(anyhow!("ODIO archive contains no readable entries"));
219        }
220
221        reader.seek(SeekFrom::Start(0))?;
222        Ok(HexenHausOdioArchive {
223            reader: Arc::new(Mutex::new(reader)),
224            entries,
225            _mark: std::marker::PhantomData,
226        })
227    }
228}
229
230impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script
231    for HexenHausOdioArchive<'b, T>
232{
233    fn default_output_script_type(&self) -> OutputScriptType {
234        OutputScriptType::Json
235    }
236
237    fn default_format_type(&self) -> FormatOptions {
238        FormatOptions::None
239    }
240
241    fn is_archive(&self) -> bool {
242        true
243    }
244
245    fn iter_archive_filename<'a>(
246        &'a self,
247    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
248        Ok(Box::new(
249            self.entries.iter().map(|entry| Ok(entry.name.clone())),
250        ))
251    }
252
253    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
254        Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
255    }
256
257    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
258        if index >= self.entries.len() {
259            return Err(anyhow!(
260                "Index out of bounds: {} (total files: {})",
261                index,
262                self.entries.len()
263            ));
264        }
265        let entry = self.entries[index].clone();
266
267        let decrypt = if entry.size >= ENTRY_HEADER_SIZE {
268            let mut header = [0u8; 4];
269            let mut guard = self
270                .reader
271                .lock()
272                .map_err(|e| anyhow!("Failed to lock reader: {}", e))?;
273            guard.seek(SeekFrom::Start(entry.offset))?;
274            guard.read_exact(&mut header)?;
275            header == *b"ONCE"
276        } else {
277            false
278        };
279
280        let (data_offset, data_size) = if decrypt {
281            let data_offset = entry
282                .offset
283                .checked_add(ENTRY_HEADER_SIZE)
284                .ok_or_else(|| anyhow!("Entry data offset overflow"))?;
285            let data_size = entry
286                .size
287                .checked_sub(ENTRY_HEADER_SIZE)
288                .ok_or_else(|| anyhow!("Entry data size underflow"))?;
289            (data_offset, data_size)
290        } else {
291            (entry.offset, entry.size)
292        };
293
294        Ok(Box::new(OdioEntry {
295            name: entry.name,
296            reader: self.reader.clone(),
297            data_offset,
298            data_size,
299            pos: 0,
300            decrypt,
301        }))
302    }
303}
304
305#[derive(Debug)]
306struct OdioEntry<T: Read + Seek> {
307    name: String,
308    reader: Arc<Mutex<T>>,
309    data_offset: u64,
310    data_size: u64,
311    pos: u64,
312    decrypt: bool,
313}
314
315impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for OdioEntry<T> {
316    fn name(&self) -> &str {
317        &self.name
318    }
319
320    fn size(&self) -> Option<u64> {
321        Some(self.data_size)
322    }
323
324    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
325        Ok(Box::new(self))
326    }
327}
328
329impl<T: Read + Seek> Read for OdioEntry<T> {
330    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
331        let total_size = self.data_size;
332        if self.pos >= total_size {
333            return Ok(0);
334        }
335
336        let remaining = total_size - self.pos;
337        let remaining_usize = match usize::try_from(remaining) {
338            Ok(value) => value,
339            Err(_) => usize::MAX,
340        };
341        let to_read = remaining_usize.min(buf.len());
342        if to_read == 0 {
343            return Ok(0);
344        }
345
346        let absolute_offset = match self.data_offset.checked_add(self.pos) {
347            Some(offset) => offset,
348            None => {
349                return Err(std::io::Error::new(
350                    std::io::ErrorKind::InvalidInput,
351                    "Read position overflow",
352                ));
353            }
354        };
355
356        let mut guard = self.reader.lock().map_err(|e| {
357            std::io::Error::new(
358                std::io::ErrorKind::Other,
359                format!("Failed to lock mutex: {}", e),
360            )
361        })?;
362        guard.seek(SeekFrom::Start(absolute_offset))?;
363        let bytes_read = guard.read(&mut buf[..to_read])?;
364        drop(guard);
365
366        if self.decrypt {
367            for byte in &mut buf[..bytes_read] {
368                *byte = byte.rotate_right(4);
369            }
370        }
371
372        self.pos = self.pos.saturating_add(bytes_read as u64);
373        Ok(bytes_read)
374    }
375}
376
377impl<T: Read + Seek> Seek for OdioEntry<T> {
378    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
379        let new_pos = match pos {
380            SeekFrom::Start(offset) => offset,
381            SeekFrom::End(offset) => {
382                let size = i64::try_from(self.data_size).map_err(|_| {
383                    std::io::Error::new(
384                        std::io::ErrorKind::InvalidInput,
385                        "Data size exceeds seek range",
386                    )
387                })?;
388                let target = size.checked_add(offset).ok_or_else(|| {
389                    std::io::Error::new(
390                        std::io::ErrorKind::InvalidInput,
391                        "Seek from end caused overflow",
392                    )
393                })?;
394                if target < 0 {
395                    return Err(std::io::Error::new(
396                        std::io::ErrorKind::InvalidInput,
397                        "Seek from end before start",
398                    ));
399                }
400                target as u64
401            }
402            SeekFrom::Current(offset) => {
403                let current = i64::try_from(self.pos).map_err(|_| {
404                    std::io::Error::new(
405                        std::io::ErrorKind::InvalidInput,
406                        "Current position overflow",
407                    )
408                })?;
409                let target = current.checked_add(offset).ok_or_else(|| {
410                    std::io::Error::new(
411                        std::io::ErrorKind::InvalidInput,
412                        "Seek from current caused overflow",
413                    )
414                })?;
415                if target < 0 {
416                    return Err(std::io::Error::new(
417                        std::io::ErrorKind::InvalidInput,
418                        "Seek before start",
419                    ));
420                }
421                target as u64
422            }
423        };
424        self.pos = new_pos;
425        Ok(self.pos)
426    }
427
428    fn stream_position(&mut self) -> std::io::Result<u64> {
429        Ok(self.pos)
430    }
431}